/*
 * Copyright (c) 2012 OptiCare Systems Ltd
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 *
 * Authors: Kevin Martin (kev82@khn.org.uk)
 */
#include <lua5.2/lua.hpp>
#include <lua5.2/lauxlib.h>
#include <string>
#include <cstdlib>
#include <iostream>
#include <cassert>
#include "gmw.hpp"
#include "gmc.hpp"
#include "luaOE.hpp"
#include <sstream>

extern "C" {
	int luaopen_gearman(lua_State *l);
	//See comment below
	int luaopen_libgearman(lua_State *l);
}

class LuaCallback : public GearmanWorkerFunction
{
private:
	int registryRef_;
	lua_State *l;

	std::string callfunc(const std::string &arg) {
		lua_rawgeti(l, LUA_REGISTRYINDEX, registryRef_);
		if(lua_type(l, -1) != LUA_TFUNCTION) {
			throw std::runtime_error("Internal - reg entry not function");
		}

		lua_pushstring(l, arg.c_str());
		if(lua_pcall(l, 1, 1, 0) != LUA_OK) {
			luaOE_throwerror(l, "Failed to run function: ");
		}

		if(lua_type(l, -1) != LUA_TSTRING) {
			throw std::runtime_error("worker didn't return string");
		}

		std::string rv = lua_tostring(l, -1);
		lua_pop(l, 1);

		return rv;
	}

protected:
	bool implementation(const std::string &arg) {
		try {
			retData_ = callfunc(arg);
			return true;
		} catch (std::exception &e) {
			retData_ = e.what();
			return false;
		}

		assert("unreachable code" == 0x0);
	}
public:
	LuaCallback(const std::string &name, int regidx, lua_State *l) :
	 GearmanWorkerFunction(name), registryRef_(regidx), l(l) {}
};

static int int_do_gearman_work(lua_State *l) {
	if(lua_type(l, lua_upvalueindex(1)) != LUA_TTABLE) {
		luaL_error(l, "upvalue corrupted");
	}

	try {
		std::vector<boost::shared_ptr<GearmanWorkerFunction> > funcs;

		int tblidx = lua_upvalueindex(1);
		lua_pushnil(l);
		while (lua_next(l, tblidx) != 0) {
			assert(lua_type(l, -2) == LUA_TSTRING);
			assert(lua_type(l, -1) == LUA_TFUNCTION);

			//This pops the function and leaves the key
			int regidx = luaL_ref(l, LUA_REGISTRYINDEX);

			//I know the key is a string, so tostring won't cause a problem
			funcs.push_back(boost::shared_ptr<GearmanWorkerFunction>(
			 new LuaCallback(lua_tostring(l, -1), regidx, l)));
		}

		GearmanWorker gw(funcs);
		gw.Run();

		return 0;
	} catch(std::exception &e) {
		return luaL_error(l, "Worker caught excpetion: %s", e.what());
	}

	assert("unreachable code" == 0x0);
	throw std::logic_error("Unraechable Code");
}

static int api_gearman_worker(lua_State *l) {
	if(lua_gettop(l) != 0) {
		luaL_error(l, "Expected no arguments");
	}

	lua_newtable(l);
	lua_pushcclosure(l, int_do_gearman_work, 1);

	return 1;
}

static int api_gearman_addfunc(lua_State *l) {
	if(lua_gettop(l) != 3) {
		luaL_error(l, "Expected 3 arguments");
	}

	if(lua_tocfunction(l,1) != int_do_gearman_work) {
		luaL_error(l, "First argument should be a gearman worker");
	}

	luaL_checktype(l, 2, LUA_TSTRING);
	luaL_checktype(l, 3, LUA_TFUNCTION);

	if(lua_getupvalue(l, 1, 1) == NULL) {
		luaL_error(l, "Internal - couldn't access upvalue");
	}

	if(lua_type(l, -1) != LUA_TTABLE) {
		luaL_error(l, "Internal - upvalue isn't table");
	}

	lua_pushvalue(l, 2);
	lua_pushvalue(l, 3);
	lua_settable(l, -3);

	return 0;
}
	
//See **TODO** below
static int int_gearman_call_local(lua_State *l) {
	assert(lua_gettop(l) == 3);

	std::string name = luaL_checkstring(l, 1);
	std::string arg = luaL_checkstring(l, 2); //gearman caller will set this to "" if nil
	int timeout = luaL_checkint(l, 3);

	try {
		std::string rv = CallGearmanLocal(name, arg, timeout);
		lua_pushstring(l, rv.c_str());
		return 1;
	} catch (std::exception &e) {
		luaL_error(l, e.what());
	}

	assert("unreachable code" == 0x0);
}

/**
 * This function will take the name of a gearman function and an optional timeout argument
 * it returns a function which when called will call the specified gearman function and
 * give an error on timeout. The timout defaults to 15 seconds.
 */
static int api_gearman_caller(lua_State *l) {
	if(lua_gettop(l) < 1 || lua_gettop(l) > 2) {
		luaL_error(l, "Expected 1 or 2 arguments");
	}

	luaL_checkstring(l, 1);

	int timeout = 15;
	if(lua_gettop(l) == 2) {
		timeout = luaL_checkint(l, 2);
	}

	// **TODO** I have realised this would be much better implemented with a c closure, but
	// I only just realised after I finished writing this - which works, so I'll leave as is for now. 
	{
		std::string code =
		 "local name, timeout, cfunc = ... return function(arg) return cfunc(name, arg or \"\", timeout) end";
		int rv = luaL_loadstring(l, code.c_str());
		assert(rv == LUA_OK);
	}

	lua_pushvalue(l, 1); //The function name
	lua_pushinteger(l, timeout);
	lua_pushcfunction(l, int_gearman_call_local);
	
	if(lua_pcall(l, 3, 1, 0) != LUA_OK) {
		std::string err = lua_tostring(l, -1);
		lua_pop(l, 1);
		luaL_error(l, err.c_str());
	}

	assert(lua_type(l, -1) == LUA_TFUNCTION);
	return 1;
}

static int api_encode_rv(lua_State *l) {
	if(lua_type(l,1) != LUA_TFUNCTION) {
		luaL_error(l, "Expected function");
	}

	if(lua_pcall(l, lua_gettop(l)-1, LUA_MULTRET, 0) != LUA_OK) {
		luaL_error(l, "Worker Failed: %s", lua_tostring(l, -1));
	}

	std::ostringstream oss;
	int nrv = lua_gettop(l);
	if(nrv>0) {
		oss << "return ";
		oss << luaOE_serialize(l, 1)->unserializeLuaString();

		for(int i=2;i!=nrv+1;++i) {
			oss << ", ";
			oss << luaOE_serialize(l, i)->unserializeLuaString();
		}
	}

	lua_pushstring(l, oss.str().c_str());
	return 1;
}

static int api_encode_args(lua_State *l) {
	std::ostringstream oss;
	int nargs = lua_gettop(l);
	if(nargs>0) {
		oss << "return ";
		oss << luaOE_serialize(l, 1)->unserializeLuaString();

		for(int i=2;i!=nargs+1;++i) {
			oss << ", ";
			oss << luaOE_serialize(l, i)->unserializeLuaString();
		}
	}

	lua_pushstring(l, oss.str().c_str());
	return 1;
}

static const luaL_Reg gearman_funcs[] = {
	{"encodeargs", api_encode_args},
	{"encodervs", api_encode_rv},
	{"worker", api_gearman_worker},
	{"addworkfunc", api_gearman_addfunc},
	{"caller", api_gearman_caller},
	{NULL, NULL}
};

int luaopen_gearman(lua_State *l) {
	luaL_newlib(l, gearman_funcs);
	return 1;
}

//This is to make it work with our internal build system which names
//things a bit differently - this will be removed when the build system
//is rethought
int luaopen_libgearman(lua_State *l) {
	return luaopen_gearman(l);
}
